/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.tomcat.util.modeler;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import javax.management.AttributeNotFoundException;
import javax.management.DynamicMBean;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanAttributeInfo;
import javax.management.MBeanConstructorInfo;
import javax.management.MBeanException;
import javax.management.MBeanInfo;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanOperationInfo;
import javax.management.ReflectionException;
import javax.management.RuntimeOperationsException;
import javax.management.ServiceNotFoundException;

//import javax.management.modelmbean.InvalidTargetObjectTypeException;

/**
 * <p>
 * Internal configuration information for a managed bean (MBean) descriptor.
 * </p>
 * 
 * @author Craig R. McClanahan
 * @version $Id: ManagedBean.java 939351 2010-04-29 15:41:54Z kkolinko $
 */

public class ManagedBean implements java.io.Serializable {
	private static final String BASE_MBEAN = "org.apache.tomcat.util.modeler.BaseModelMBean";
	// ----------------------------------------------------- Instance Variables
	static final Object[] NO_ARGS_PARAM = new Object[0];
	static final Class[] NO_ARGS_PARAM_SIG = new Class[0];

	/**
	 * The <code>ModelMBeanInfo</code> object that corresponds to this
	 * <code>ManagedBean</code> instance.
	 */
	transient MBeanInfo info = null;
	// Map<AttributeInfo>
	private Map attributes = new HashMap();
	// Map<OperationInfo>
	private Map operations = new HashMap();

	protected String className = BASE_MBEAN;
	// protected ConstructorInfo constructors[] = new ConstructorInfo[0];
	protected String description = null;
	protected String domain = null;
	protected String group = null;
	protected String name = null;

	// protected List fields = new ArrayList();
	protected NotificationInfo notifications[] = new NotificationInfo[0];
	protected String type = null;

	/**
	 * Constructor. Will add default attributes.
	 * 
	 */
	public ManagedBean() {
		AttributeInfo ai = new AttributeInfo();
		ai.setName("modelerType");
		ai.setDescription("Type of the modeled resource. Can be set only once");
		ai.setType("java.lang.String");
		ai.setWriteable(false);
		addAttribute(ai);
	}

	// ------------------------------------------------------------- Properties

	/**
	 * The collection of attributes for this MBean.
	 */
	public AttributeInfo[] getAttributes() {
		AttributeInfo result[] = new AttributeInfo[attributes.size()];
		attributes.values().toArray(result);
		return result;
	}

	/**
	 * The fully qualified name of the Java class of the MBean described by this
	 * descriptor. If not specified, the standard JMX class (
	 * <code>javax.management.modelmbean.RequiredModeLMBean</code>) will be
	 * utilized.
	 */
	public String getClassName() {
		return (this.className);
	}

	public void setClassName(String className) {
		this.className = className;
		this.info = null;
	}

	// /**
	// * The collection of constructors for this MBean.
	// */
	// public ConstructorInfo[] getConstructors() {
	// return (this.constructors);
	// }

	/**
	 * The human-readable description of this MBean.
	 */
	public String getDescription() {
		return (this.description);
	}

	public void setDescription(String description) {
		this.description = description;
		this.info = null;
	}

	/**
	 * The (optional) <code>ObjectName</code> domain in which this MBean should
	 * be registered in the MBeanServer.
	 */
	public String getDomain() {
		return (this.domain);
	}

	public void setDomain(String domain) {
		this.domain = domain;
	}

	/**
	 * <p>
	 * Return a <code>List</code> of the {@link FieldInfo} objects for the
	 * name/value pairs that should be added to the Descriptor created from this
	 * metadata.
	 * </p>
	 */
	// public List getFields() {
	// return (this.fields);
	// }
	//
	/**
	 * The (optional) group to which this MBean belongs.
	 */
	public String getGroup() {
		return (this.group);
	}

	public void setGroup(String group) {
		this.group = group;
	}

	/**
	 * The name of this managed bean, which must be unique among all MBeans
	 * managed by a particular MBeans server.
	 */
	public String getName() {
		return (this.name);
	}

	public void setName(String name) {
		this.name = name;
		this.info = null;
	}

	/**
	 * The collection of notifications for this MBean.
	 */
	public NotificationInfo[] getNotifications() {
		return (this.notifications);
	}

	/**
	 * The collection of operations for this MBean.
	 */
	public OperationInfo[] getOperations() {
		OperationInfo[] result = new OperationInfo[operations.size()];
		operations.values().toArray(result);
		return result;
	}

	/**
	 * The fully qualified name of the Java class of the resource implementation
	 * class described by the managed bean described by this descriptor.
	 */
	public String getType() {
		return (this.type);
	}

	public void setType(String type) {
		this.type = type;
		this.info = null;
	}

	// --------------------------------------------------------- Public Methods

	/**
	 * Add a new attribute to the set of attributes for this MBean.
	 * 
	 * @param attribute
	 *            The new attribute descriptor
	 */
	public void addAttribute(AttributeInfo attribute) {
		attributes.put(attribute.getName(), attribute);
	}

	/**
	 * Add a new constructor to the set of constructors for this MBean.
	 * 
	 * @param constructor
	 *            The new constructor descriptor
	 */
	// public void addConstructor(ConstructorInfo constructor) {
	//
	// synchronized (constructors) {
	// ConstructorInfo results[] =
	// new ConstructorInfo[constructors.length + 1];
	// System.arraycopy(constructors, 0, results, 0, constructors.length);
	// results[constructors.length] = constructor;
	// constructors = results;
	// this.info = null;
	// }
	//
	// }

	/**
	 * <p>
	 * Add a new field to the fields associated with the Descriptor that will be
	 * created from this metadata.
	 * </p>
	 * 
	 * @param field
	 *            The field to be added
	 */
	// public void addField(FieldInfo field) {
	// fields.add(field);
	// }

	/**
	 * Add a new notification to the set of notifications for this MBean.
	 * 
	 * @param notification
	 *            The new notification descriptor
	 */
	public void addNotification(NotificationInfo notification) {

		synchronized (notifications) {
			NotificationInfo results[] = new NotificationInfo[notifications.length + 1];
			System
					.arraycopy(notifications, 0, results, 0,
							notifications.length);
			results[notifications.length] = notification;
			notifications = results;
			this.info = null;
		}

	}

	/**
	 * Add a new operation to the set of operations for this MBean.
	 * 
	 * @param operation
	 *            The new operation descriptor
	 */
	public void addOperation(OperationInfo operation) {
		operations.put(operation.getName(), operation);
	}

	/**
	 * Create and return a <code>ModelMBean</code> that has been preconfigured
	 * with the <code>ModelMBeanInfo</code> information for this managed bean,
	 * but is not associated with any particular managed resource. The returned
	 * <code>ModelMBean</code> will <strong>NOT</strong> have been registered
	 * with our <code>MBeanServer</code>.
	 * 
	 * @exception InstanceNotFoundException
	 *                if the managed resource object cannot be found
	 * @exception InvalidTargetObjectTypeException
	 *                if our MBean cannot handle object references (should never
	 *                happen)
	 * @exception MBeanException
	 *                if a problem occurs instantiating the
	 *                <code>ModelMBean</code> instance
	 * @exception RuntimeOperationsException
	 *                if a JMX runtime error occurs
	 */
	public DynamicMBean createMBean() throws InstanceNotFoundException,
			MBeanException, RuntimeOperationsException {

		return (createMBean(null));

	}

	/**
	 * Create and return a <code>ModelMBean</code> that has been preconfigured
	 * with the <code>ModelMBeanInfo</code> information for this managed bean,
	 * and is associated with the specified managed object instance. The
	 * returned <code>ModelMBean</code> will <strong>NOT</strong> have been
	 * registered with our <code>MBeanServer</code>.
	 * 
	 * @param instance
	 *            Instanced of the managed object, or <code>null</code> for no
	 *            associated instance
	 * 
	 * @exception InstanceNotFoundException
	 *                if the managed resource object cannot be found
	 * @exception InvalidTargetObjectTypeException
	 *                if our MBean cannot handle object references (should never
	 *                happen)
	 * @exception MBeanException
	 *                if a problem occurs instantiating the
	 *                <code>ModelMBean</code> instance
	 * @exception RuntimeOperationsException
	 *                if a JMX runtime error occurs
	 */
	public DynamicMBean createMBean(Object instance)
			throws InstanceNotFoundException, MBeanException,
			RuntimeOperationsException {

		BaseModelMBean mbean = null;

		// Load the ModelMBean implementation class
		if (getClassName().equals(BASE_MBEAN)) {
			// Skip introspection
			mbean = new BaseModelMBean();
		} else {
			Class clazz = null;
			Exception ex = null;
			try {
				clazz = Class.forName(getClassName());
			} catch (Exception e) {
			}

			if (clazz == null) {
				try {
					ClassLoader cl = Thread.currentThread()
							.getContextClassLoader();
					if (cl != null)
						clazz = cl.loadClass(getClassName());
				} catch (Exception e) {
					ex = e;
				}
			}

			if (clazz == null) {
				throw new MBeanException(ex, "Cannot load ModelMBean class "
						+ getClassName());
			}
			try {
				// Stupid - this will set the default minfo first....
				mbean = (BaseModelMBean) clazz.newInstance();
			} catch (RuntimeOperationsException e) {
				throw e;
			} catch (Exception e) {
				throw new MBeanException(e,
						"Cannot instantiate ModelMBean of class "
								+ getClassName());
			}
		}

		mbean.setManagedBean(this);

		// Set the managed resource (if any)
		try {
			if (instance != null)
				mbean.setManagedResource(instance, "ObjectReference");
		} catch (InstanceNotFoundException e) {
			throw e;
		}
		return (mbean);

	}

	/**
	 * Create and return a <code>ModelMBeanInfo</code> object that describes
	 * this entire managed bean.
	 */
	MBeanInfo getMBeanInfo() {

		// Return our cached information (if any)
		if (info != null)
			return (info);

		// Create subordinate information descriptors as required
		AttributeInfo attrs[] = getAttributes();
		MBeanAttributeInfo attributes[] = new MBeanAttributeInfo[attrs.length];
		for (int i = 0; i < attrs.length; i++)
			attributes[i] = attrs[i].createAttributeInfo();

		OperationInfo opers[] = getOperations();
		MBeanOperationInfo operations[] = new MBeanOperationInfo[opers.length];
		for (int i = 0; i < opers.length; i++)
			operations[i] = opers[i].createOperationInfo();

		// ConstructorInfo consts[] = getConstructors();
		// ModelMBeanConstructorInfo constructors[] =
		// new ModelMBeanConstructorInfo[consts.length];
		// for (int i = 0; i < consts.length; i++)
		// constructors[i] = consts[i].createConstructorInfo();

		NotificationInfo notifs[] = getNotifications();
		MBeanNotificationInfo notifications[] = new MBeanNotificationInfo[notifs.length];
		for (int i = 0; i < notifs.length; i++)
			notifications[i] = notifs[i].createNotificationInfo();

		// Construct and return a new ModelMBeanInfo object
		info = new MBeanInfo(getClassName(), getDescription(), attributes,
				new MBeanConstructorInfo[] {}, operations, notifications);
		// try {
		// Descriptor descriptor = info.getMBeanDescriptor();
		// Iterator fields = getFields().iterator();
		// while (fields.hasNext()) {
		// FieldInfo field = (FieldInfo) fields.next();
		// descriptor.setField(field.getName(), field.getValue());
		// }
		// info.setMBeanDescriptor(descriptor);
		// } catch (MBeanException e) {
		// ;
		// }

		return (info);

	}

	/**
	 * Return a string representation of this managed bean.
	 */
	public String toString() {

		StringBuffer sb = new StringBuffer("ManagedBean[");
		sb.append("name=");
		sb.append(name);
		sb.append(", className=");
		sb.append(className);
		sb.append(", description=");
		sb.append(description);
		if (group != null) {
			sb.append(", group=");
			sb.append(group);
		}
		sb.append(", type=");
		sb.append(type);
		sb.append("]");
		return (sb.toString());

	}

	Method getGetter(String aname, BaseModelMBean mbean, Object resource)
			throws AttributeNotFoundException, MBeanException,
			ReflectionException {
		// TODO: do we need caching ? JMX is for management, it's not supposed
		// to require lots of performance.
		Method m = null; // (Method)getAttMap.get( name );

		if (m == null) {
			AttributeInfo attrInfo = (AttributeInfo) attributes.get(aname);
			// Look up the actual operation to be used
			if (attrInfo == null)
				throw new AttributeNotFoundException(" Cannot find attribute "
						+ aname + " for " + resource);

			String getMethod = attrInfo.getGetMethod();
			if (getMethod == null)
				throw new AttributeNotFoundException("Cannot find attribute "
						+ aname + " get method name");

			Object object = null;
			NoSuchMethodException exception = null;
			try {
				object = mbean;
				m = object.getClass().getMethod(getMethod, NO_ARGS_PARAM_SIG);
			} catch (NoSuchMethodException e) {
				exception = e;
				;
			}
			if (m == null && resource != null) {
				try {
					object = resource;
					m = object.getClass().getMethod(getMethod,
							NO_ARGS_PARAM_SIG);
					exception = null;
				} catch (NoSuchMethodException e) {
					exception = e;
				}
			}
			if (exception != null)
				throw new ReflectionException(exception,
						"Cannot find getter method " + getMethod);
			// getAttMap.put( name, m );
		}

		return m;
	}

	public Method getSetter(String aname, BaseModelMBean bean, Object resource)
			throws AttributeNotFoundException, MBeanException,
			ReflectionException {
		// Cache may be needed for getters, but it is a really bad idea for
		// setters, this is far
		// less frequent.
		Method m = null;// (Method)setAttMap.get( name );

		if (m == null) {
			AttributeInfo attrInfo = (AttributeInfo) attributes.get(aname);
			if (attrInfo == null)
				throw new AttributeNotFoundException(" Cannot find attribute "
						+ aname);

			// Look up the actual operation to be used
			String setMethod = attrInfo.getSetMethod();
			if (setMethod == null)
				throw new AttributeNotFoundException("Cannot find attribute "
						+ aname + " set method name");

			String argType = attrInfo.getType();

			Class signature[] = new Class[] { BaseModelMBean
					.getAttributeClass(argType) };

			Object object = null;
			NoSuchMethodException exception = null;
			try {
				object = bean;
				m = object.getClass().getMethod(setMethod, signature);
			} catch (NoSuchMethodException e) {
				exception = e;
				;
			}
			if (m == null && resource != null) {
				try {
					object = resource;
					m = object.getClass().getMethod(setMethod, signature);
					exception = null;
				} catch (NoSuchMethodException e) {
					exception = e;
				}
			}
			if (exception != null)
				throw new ReflectionException(exception,
						"Cannot find setter method " + setMethod + " "
								+ resource);
			// setAttMap.put( name, m );
		}

		return m;
	}

	public Method getInvoke(String aname, Object[] params, String[] signature,
			BaseModelMBean bean, Object resource) throws MBeanException,
			ReflectionException {
		Method method = null;
		if (method == null) {
			if (params == null)
				params = new Object[0];
			if (signature == null)
				signature = new String[0];
			if (params.length != signature.length)
				throw new RuntimeOperationsException(
						new IllegalArgumentException(
								"Inconsistent arguments and signature"),
						"Inconsistent arguments and signature");

			// Acquire the ModelMBeanOperationInfo information for
			// the requested operation
			OperationInfo opInfo = (OperationInfo) operations.get(aname);
			if (opInfo == null)
				throw new MBeanException(new ServiceNotFoundException(
						"Cannot find operation " + aname),
						"Cannot find operation " + aname);

			// Prepare the signature required by Java reflection APIs
			// FIXME - should we use the signature from opInfo?
			Class types[] = new Class[signature.length];
			for (int i = 0; i < signature.length; i++) {
				types[i] = BaseModelMBean.getAttributeClass(signature[i]);
			}

			// Locate the method to be invoked, either in this MBean itself
			// or in the corresponding managed resource
			// FIXME - Accessible methods in superinterfaces?
			Object object = null;
			Exception exception = null;
			try {
				object = bean;
				method = object.getClass().getMethod(aname, types);
			} catch (NoSuchMethodException e) {
				exception = e;
				;
			}
			try {
				if ((method == null) && (resource != null)) {
					object = resource;
					method = object.getClass().getMethod(aname, types);
				}
			} catch (NoSuchMethodException e) {
				exception = e;
			}
			if (method == null) {
				throw new ReflectionException(exception, "Cannot find method "
						+ aname + " with this signature");
			}
			// invokeAttMap.put(mkey, method);
		}
		return method;
	}

}
